﻿using System;
using System.Collections.Generic;
using System.Threading;

using Conf = OCXS.OCXSServiceLibrary.Configuration;
using OCXS.OCXSServiceLibrary.Utils;

using System.IO;

namespace OCXS.OCXSServiceLibrary {

    /// <summary>
    /// Represents a record of the <c><see cref="OCXS.OCXSServiceLibrary.OCXSService.subscriptionTable"/></c>.
    /// </summary>
    /// <remarks>
    /// <para>
    /// A <c>SubscriptionRecord</c> represents an allocated resource for a requested OPC Subscription. 
    /// It, formerly, represents the OPC Subscription entity.
    /// </para>
    /// <para>
    /// Every time a new OPC Subscription is requested, a new <c>SubscriptionRecord</c> is created and added to
    /// the <c><see cref="OCXS.OCXSServiceLibrary.OCXSService.subscriptionTable"/></c>.
    /// </para>
    /// </remarks>
    public class SubscriptionRecord : IDisposable {

        /// <summary>
        /// GUID for the current subscription.
        /// </summary>
        /// <remarks>
        /// The identifier is in the form of a Microsoft Registry-Formatted GUID. A new random one is automatically
        /// created at object construction.
        /// </remarks>
        private string sid;

        /// <summary>
        /// Array of <c><see cref="OCXS.OCXSServiceLibrary.WatchingUnit"/></c> representing allocated resources for
        /// all subscried OPC Items in the Subscription.
        /// </summary>
        /// <remarks>
        /// Each entry is bound to an OPC Item being subscribed.
        /// </remarks>
        private WatchingUnit[] watchingUnits;

        /// <summary>
        /// Timeout timer to manage OPC Subscription PingRate expiration.
        /// </summary>
        /// <remarks>
        /// The timer is initialized during object construction, but not activated. After creating the <c>SubscriptionRecord</c>
        /// user must activate the timer manually through the <c><see cref="OCXS.OCXSServiceLibrary.SubscriptionRecord.StartRelease"/></c>
        /// method.
        /// </remarks>
        private System.Timers.Timer selfRelease;

        /// <summary>
        /// Specifies whether or not the current subscription resources have been disposed.  
        /// </summary>
        /// <remarks>
        /// <para>
        /// Resource disposal can occur because of either ping rate timeout expiration or subsription cancel call.
        /// </para>
        /// <para>
        /// When a subscription is disposed, it is not permanently removed from the 
        /// <c><see cref="OCXS.OCXSServiceLibrary.OCXSService.subscriptionTable"/></c>, the internal 
        /// subscription garbage collector will proceed removing all disposed entries 
        /// (looking for the subscription <c>Disposed</c> field).
        /// </para>
        /// </remarks>
        private bool disposed;

        /// <summary>
        /// Gets the GUID for the current subscription.
        /// </summary>
        public string SID {
            get { return this.sid; }
        }

        /// <summary>
        /// Gets the disposal status of the subscription.
        /// </summary>
        public bool Disposed {
            get { return this.disposed; }
        }

        /// <summary>
        /// Gets the array of variables being monitored in the subscription.
        /// </summary>
        /// <remarks>
        /// Every subscription has, internally, a set of variables being monitored 
        /// (corresponding to those items that have been subscribed in the subscription). 
        /// This property gets the complete name of watched variables.
        /// </remarks>
        public string[] WatchedVariables {
            get {
                string[] res = new string[this.watchingUnits.Length];

                for (int i = 0; i < this.watchingUnits.Length; i++)
                    res[i] = this.watchingUnits[i].WatchedVariableID;

                return res;
            }
        }

        /// <summary>
        /// Default Constructor.
        /// </summary>
        /// <remarks>
        /// It is not possible constructing a <c>SubScriptionRecord</c> using this constructor.
        /// </remarks>
        private SubscriptionRecord() {
            this.sid = Guid.NewGuid().ToString();
            this.disposed = false;
        }

        /// <summary>
        /// Constructs a new instance of the SubscriptionRecord class allocating resources for
        /// the requested subscription.
        /// </summary>
        /// <remarks>
        /// <para>
        /// All watched variables belonging to the same subscription share the same period.
        /// </para>
        /// <para>
        /// An array of float is passed in order to specify deadbands for each variable requested.
        /// Deadbands are matched 1-to-1 with variableIds.
        /// </para>
        /// <para>
        /// The <c>selfRelease</c> timer does not start automatically at object construction! You need to do it manually
        /// calling the provided API.
        /// </para>
        /// </remarks>
        /// <param name="variableIds">An array containing requested variable identifiers.</param>
        /// <param name="dataStore">The pointer to variable pool. This is used for simulation purposes.</param>
        /// <param name="period">
        /// The time interval expressed in milliseconds between two subsequent watching unit variable checks.
        /// </param>
        /// <param name="deadbands">The array of deadbands to be applied (in the corresponding order) to the watched variables.</param>
        /// <param name="pingRate">The highest subscription polling rate to keep the subscription valid.</param>
        public SubscriptionRecord(
                string[] variableIds, 
                Dictionary<String, KeyValuePair<Conf.VariableType, Object>> dataStore, 
                int period,
                double pingRate,
                float[] deadbands = null
                ) : this() {

            // Creating watching unit array
            this.watchingUnits = new WatchingUnit[variableIds.Length];

            // Initialize timer
            this.selfRelease = new System.Timers.Timer(pingRate);
            this.selfRelease.AutoReset = false;
            this.selfRelease.Elapsed += this.SelfReleaseElapsed;
            
            /* For each variable id provided, look for the corresponding resource 
             * in the variable pool and, if a match is found, allocate a watching unit for it. 
             * If no variable is found throw a SubscriptionRecordException. 
             */
            for (int i = 0; i < variableIds.Length; i++) {

                KeyValuePair< Conf.VariableType, Object > v;

                if (dataStore.TryGetValue(variableIds[i], out v)) {

                    // Valid
                    this.watchingUnits[i] = new WatchingUnit(v, 
                        period, 
                        this.sid, 
                        (deadbands == null) ? 0.0f : deadbands[i]
                        );
                    this.watchingUnits[i].Start();

                }
            }
        } /* Ctor */

        /// <summary>
        /// Destructor.
        /// </summary>
        /// <remarks>
        /// This destructor will run only if the Dispose method
        /// does not get called.
        /// </remarks>
        ~SubscriptionRecord() {
            this.Dispose();
        } /* DCtor */

        /// <summary>
        /// Returns the Last Changed Values (LCVs) for each <c>WatchedVariable</c> being monitored
        /// by the subscription.
        /// </summary>
        /// <remarks>
        /// Attention, this call causes each <c>WatchedVariable</c>'s buffer to be locked.
        /// </remarks>
        /// <returns>The Last Changed Values (LCVs) for each <c>WatchedVariable</c> being monitored
        /// by the subscription.</returns>
        public PolledRefreshVariableLCV[] GetLCVs() {

            PolledRefreshVariableLCV[] lcvs = new PolledRefreshVariableLCV[this.watchingUnits.Length];

            // Getting values
            for (int i = 0; i < this.watchingUnits.Length; i++)
                lcvs[i] = this.watchingUnits[i].GetBufferedValueAndChangeInfo();

            return lcvs;

        } /* GetLCV */

        /// <summary>
        /// Provides disposal functionalities for the Subscription.
        /// </summary>
        /// <remarks>
        /// <para>
        /// <c>SubscriptionRecord</c> disposal results in each <c>WatchedVariable</c>'s underlying thread
        /// to be stopped, and its buffer discarded.
        /// </para>
        /// <para>
        /// This method is called when an OPC Subscription is canceled or times out.
        /// </para>
        /// <para>
        /// Calling this method prevents the .NET Garbage Collector to call Destructor.
        /// </para>
        /// </remarks>
        public void Dispose() {
            foreach (WatchingUnit w in this.watchingUnits)
                w.Stop();
            
            // Mark as disposed for Subscription Collector.
            this.disposed = true;

            GC.SuppressFinalize(this);
        }

        /// <summary>
        /// Starts the OPC Subscription timer.
        /// </summary>
        public void StartRelease() {
            this.selfRelease.Start();
        }

        /// <summary>
        /// Stops the OPC Subscription timer.
        /// </summary>
        public void StopRelease() {
            this.selfRelease.Stop();
        }

        /// <summary>
        /// Subscription timer elapsing event handler.
        /// </summary>
        /// <remarks>
        /// <para>
        /// When the Subscription timer expires this method is invoked.
        /// </para>
        /// <para>
        /// Calling this method causes the <c>SubscriptionRecord</c> to be disposed.
        /// </para>
        /// </remarks>
        /// <param name="sender">Sender object.</param>
        /// <param name="e">Event arguments.</param>
        private void SelfReleaseElapsed(object sender, System.Timers.ElapsedEventArgs e) { 
            this.Dispose();
        }

    } /* SubscriptionRecord */

    /// <summary>
    /// Represents the exception raised when a Subscription request contains an invalid
    /// variable identificator.
    /// </summary>
    /// <remarks>
    /// Orphan. To be used for future implementations.
    /// </remarks>
    public class SubscriptionRecordException : Exception {

        /// <summary>
        /// Default Constructor.
        /// </summary>
        public SubscriptionRecordException() : base() {
        }

        /// <summary>
        /// Constructor reporting a specified message.
        /// </summary>
        /// <param name="message">Specified message.</param>
        public SubscriptionRecordException(string message) : base(message) {
        }

    }

    /// <summary>
    /// Represents subscription entry, corresponding to an OPC Item being watched.
    /// </summary>
    /// <remarks>
    /// Each <c>WatchingUnit</c> is composed by a <c>Thread</c>, a 
    /// <c><see cref="OCXS.OCXSServiceLibrary.Utils.LimitedBuffer&lt;T&gt;"/></c>, storing evolving variable values, and a <c>bool</c>
    /// flag, which let the Service know whether or not current variable value has changed.
    /// </remarks>
    public class WatchingUnit : IDisposable {

        /// <summary>
        /// Default maximum <c><see cref="OCXS.OCXSServiceLibrary.Utils.LimitedBuffer&lt;T&gt;"/></c> size.
        /// </summary>
        public const int BufferLimit = 10;

        /// <summary>
        /// Locking object for the internal buffer.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Each time the buffer needs to be read or written, this object is locked providing
        /// mutual exclusion to threads.
        /// </para>
        /// <para>
        /// This object provides guarding functionality for either <c><see cref="OCXS.OCXSServiceLibrary.WatchingUnit.buffer"/></c>
        /// and <c><see cref="OCXS.OCXSServiceLibrary.WatchingUnit.changed"/></c>.
        /// </para>
        /// </remarks>
        private object padlock;

        /// <summary>
        /// Buffer storing evolving watched variable values.
        /// </summary>
        private LimitedBuffer<float> buffer;

        /// <summary>
        /// Thread meant to watch the variable.
        /// </summary>
        /// <remarks>
        /// The thread executes the <c><see cref="OCXS.OCXSServiceLibrary.WatchingUnit.Watch"/></c> method.
        /// </remarks>
        private Thread watcher;

        /// <summary>
        /// Event wait handle identifier.
        /// </summary>
        /// <remarks>
        /// <para>
        /// This unique system-wide wait handle identifier is used to create a shared <c>EventWaitHandle</c>
        /// to awake SubscriptionPolledRefresh during the wait phase (Advanced Polling approach).
        /// </para>
        /// <para>
        /// Each time a new value is written in the buffer, the <c>EventWaitHandle</c> having the specified
        /// system-wide name is signaled.
        /// </para>
        /// </remarks>
        private string ewhId;

        /// <summary>
        /// Time interval, in milliseconds, between two subsequent variable checks.
        /// </summary>
        /// <remarks>
        /// This value is used for SamplingRate implementation.
        /// </remarks>
        private int watcherPeriod;

        /// <summary>
        /// Specifies whether or not most recent variable value has changed (LCV).
        /// </summary>
        /// <remarks>
        /// The Subscription Service, during a SubscriptionPolledRefresh call, checks this value
        /// to retrieve updated variables.
        /// </remarks>
        private bool changed;

        /// <summary>
        /// Interval, formuled in probability notation for percentage (0..1), which denotes
        /// whether or not a variable change should be considered significant.
        /// </summary>
        /// <remarks>
        /// Basing on this parameter, the underlying thread will write on buffer only when
        /// the variable value change ratio exceeds the <c>deadband</c>.
        /// </remarks>
        private float deadband;

        /// <summary>
        /// Reference to the variable being watched in the pool.
        /// </summary>
        /// <remarks>
        /// This object is used to directly reference the entry in the variable pool 
        /// (<c><see cref="OCXS.OCXSServiceLibrary.OCXSService.dataStore"/></c>).
        /// </remarks>
        private KeyValuePair< Conf.VariableType, Object > variable;

        /// <summary>
        /// Gets the <c>WatchingUnit</c> changed status.
        /// </summary>
        public bool Changed {
            get {

                lock (this.padlock) {
                    return this.changed;
                }

            }
        }

        /// <summary>
        /// Gets the watched variable identifier (path + name).
        /// </summary>
        public string WatchedVariableID {
            get { return this.variable.Key.VariablePath + this.variable.Key.VariableName; }
        }

        /// <summary>
        /// Gets the variable watcher check period.
        /// </summary>
        public int WatcherPeriod {
            get { return this.watcherPeriod; }
        }
        
        /// <summary>
        /// Default Constructor.
        /// </summary>
        /// <remarks>
        /// It is not possible constructing a <c>WatchingUnit</c> using this constructor.
        /// </remarks>
        private WatchingUnit() {
            this.padlock = new Object();
            this.buffer = new LimitedBuffer<float>(BufferLimit);
            this.watcher = new Thread(this.Watch);
            this.changed = false;
            this.watcherPeriod = 5000;
        }

        /// <summary>
        /// Initializes a new instance of the <c>WatchingUnit</c> class.
        /// </summary>
        /// <remarks>
        /// The object attributes will be initialized with provided values.
        /// </remarks>
        /// <param name="variable">Reference to the variable to be watched in the pool 
        /// (<c><see cref="OCXS.OCXSServiceLibrary.OCXSService.dataStore"/></c>).</param>
        /// <param name="period">Time interval, in milliseconds, between two subsequent variable checks.</param>
        /// <param name="ewhid"><c>EventWaitHandle</c> system-wide identifier.</param>
        /// <param name="deadband">
        /// Interval, formuled in probability notation for percentage (0..1), which denotes
        /// whether or not the variable change should be considered significant.
        /// </param>
        public WatchingUnit(
                KeyValuePair< Conf.VariableType, Object > variable, 
                int period, 
                string ewhid, 
                float deadband = 0.0f
                ) : this() {

            this.variable = variable;
            this.watcherPeriod = period;
            this.ewhId = ewhid;
            this.deadband = deadband;
        }

        /// <summary>
        /// Destructor.
        /// </summary>
        /// <remarks>
        /// This destructor will run only if the Dispose method
        /// does not get called.
        /// </remarks>
        ~WatchingUnit() {
            this.Dispose();
        }

        /// <summary>
        /// Returns the Last Changed Value (LCV) in buffer.
        /// </summary>
        /// <remarks>
        /// This call will cause <c>padlock</c> to be locked, granting mutual exclusive
        /// access to shared resource.
        /// </remarks>
        /// <returns>The most recent value in buffer.</returns>
        public float GetBufferedValue() {

            float val = 0;

            lock (this.padlock) {

                val = this.buffer.Value;
                this.changed = false;

            }

            return val;

        }

        /// <summary>
        /// Returns a <c><see cref="OCXS.OCXSServiceLibrary.Utils.PolledRefreshVariableLCV"/></c>
        /// corresponding to the most recent value in buffer.
        /// </summary>
        /// <remarks>
        /// This call will cause <c>padlock</c> to be locked, granting mutual exclusive
        /// access to shared resource.
        /// </remarks>
        /// <returns>A <c>PolledRefreshVariableLCV</c> containing requestedi information.</returns>
        public PolledRefreshVariableLCV GetBufferedValueAndChangeInfo() {

            float val = 0;
            bool c = false;

            lock (this.padlock) {

                val = this.buffer.Value;
                c = this.changed;
                this.changed = false;

            }

            return new PolledRefreshVariableLCV(this.variable.Key.VariablePath, this.variable.Key.VariableName, val, c);

        }

        /// <summary>
        /// Starts watcher thread.
        /// </summary>
        public void Start() {
            this.watcher.Start();
        }

        /// <summary>
        /// Stops watcher thread.
        /// </summary>
        public void Stop() {
            this.watcher.Abort();
        }

        /// <summary>
        /// Watcher thread execution flow member function.
        /// </summary>
        /// <remarks>
        /// <para>
        /// Every watcher period, watcher thread reads data from variable in the pool
        /// (<c><see cref="OCXS.OCXSServiceLibrary.OCXSService.dataStore"/></c>). 
        /// </para>
        /// <para>
        /// If variable has changed, considering the deadband modifiers, new data is 
        /// buffered and <c>EventWaitHandle</c> signaled.
        /// </para>
        /// </remarks>
        [STAThread]
        private void Watch() {

            EventWaitHandle ewh = new EventWaitHandle(false, EventResetMode.ManualReset, this.ewhId);
            bool foo = false;

            try {

                /**
                 * You can add a logger uncommenting the following lines here and below:
                 * 
                 * StreamWriter SW = new StreamWriter(@"PATH_TO_YOUR_SERVICE" + this.variable.Key.VariableName + "_" + 
                 *                                    this.ewhId.ToString() + ".log", false);
                 * SW.WriteLine("Sleeping for: " + this.watcherPeriod);
                 */
                while (true) {

                    /**
                     * SW.WriteLine("AWAKE " + DateTime.Now);
                     */

                    // Operating
                    float val = 0;

                    // Getting value
                    lock (this.variable.Value) {

                        val = (float)this.variable.Key.VariableValue;

                    }

                    // Buffering
                    lock (this.padlock) {

                        // Getting old value
                        float currentVal = this.buffer.Value;
                        if (Math.Abs(currentVal - val) > currentVal * this.deadband) {
                            this.buffer.Value = val;
                            this.changed = true;
                            foo = true;

                            /**
                             * SW.WriteLine(@"\tCURRENT VAL: " + currentVal +
                             *            @"\tVAL        : " + val +
                             *            @"\tCONDITION  : " + Math.Abs(currentVal - val) + " >= " + currentVal * this.deadband +
                             *            @"\tCHANGED    : " + this.changed);
                             */
                        }

                    }

                    if (!ewh.WaitOne(0) && foo) {
                        foo = false;
                        ewh.Set(); /* Signal */
                        /**
                         * SW.WriteLine(@"\t***SIGNALED***");
                         */
                    }

                    /**
                     * SW.WriteLine(@"----zZzZzZ-----\n\n");
                     * SW.Flush();
                     */
                    // Suspend
                    Thread.Sleep(this.watcherPeriod);

                }

            } catch (ThreadAbortException e) {

                // An abort from above has been called --> finish the execution
                return;

            } catch (Exception e) {

                // Unexpected error
                return;

            }

        }

        /// <summary>
        /// Provides disposal functionalities for the <c>WatchingUnit</c>.
        /// </summary>
        /// <remarks>
        /// <para>
        /// <c>WatchingUnit</c> disposal results in the underlying thread
        /// to be stopped, and the buffer discarded.
        /// </para>
        /// <para>
        /// Tipically, this method is called following the <c>SubscriptionRecord</c> 
        /// disposal chain (<c><see cref="OCXS.OCXSServiceLibrary.SubscriptionRecord.Dispose"/></c>).
        /// </para>
        /// <para>
        /// Calling this method prevents the .NET Garbage Collector to call Destructor.
        /// </para>
        /// </remarks>
        public void Dispose() {
            this.watcher = null;
            this.buffer = null;

            GC.SuppressFinalize(this);
        }

    } /* WatchingUnit */

} /* OCXS.OCXSServiceLibrary */
